/*
 *   Copyright (c) 2023-2025 R3BL LLC
 *   All rights reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

use crossterm::{cursor::{MoveToColumn, MoveToNextLine, MoveToPreviousLine},
                style::{Print, ResetColor, SetBackgroundColor, SetForegroundColor},
                terminal::{Clear, ClearType}};
use miette::IntoDiagnostic;

use crate::{AnsiStyledText, ChUnit, CommonResult, DEVELOPMENT_MODE, FunctionComponent,
            GCStringOwned, Header, HowToChoose, InlineString, InlineVec, OutputDevice,
            State, StyleSheet, TuiStyle, ast, ch, choose_apply_style, col,
            core::common::string_repeat_cache::get_spaces, fg_blue, get_terminal_width,
            inline_string, lock_output_device_as_mut, queue_commands, usize, width};

#[allow(missing_debug_implementations)]
pub struct SelectComponent {
    pub output_device: OutputDevice,
    pub style: StyleSheet,
}

const IS_FOCUSED: &str = " › ";
const IS_NOT_FOCUSED: &str = "   ";
const MULTI_SELECT_IS_SELECTED: &str = "✔";
const MULTI_SELECT_IS_NOT_SELECTED: &str = "☐";
const SINGLE_SELECT_IS_SELECTED: &str = "◉";
const SINGLE_SELECT_IS_NOT_SELECTED: &str = "◌";

impl FunctionComponent<State> for SelectComponent {
    fn get_output_device(&mut self) -> OutputDevice { self.output_device.clone() }

    // Header can be either a single line or a multi line.
    fn calculate_header_viewport_height(&self, state: &mut State) -> ChUnit {
        match state.header {
            Header::SingleLine(_) => ch(1),
            Header::MultiLine(ref lines) => ch(lines.len()),
        }
    }

    /// If there are more items than the max display height, then we only use max display
    /// height. Otherwise we can shrink the display height to the number of items.
    /// This does NOT include the header.
    fn calculate_items_viewport_height(&self, state: &mut State) -> ChUnit {
        if state.items.len() > usize(state.max_display_height) {
            state.max_display_height
        } else {
            ch(state.items.len())
        }
    }

    /// Allocate space and print the lines. The bring the cursor back to the start of the
    /// lines.
    fn render(&mut self, state: &mut State) -> CommonResult<()> {
        let render_context = render_helper::RenderContext::new(self, state);

        render_helper::log_render_debug_info(state, &render_context);

        self.allocate_viewport_height_space(state)?;

        render_helper::render_header(
            &mut self.output_device,
            &state.header,
            &self.style.header_style,
            render_context.viewport_width,
            render_context.start_display_col_offset,
        )?;

        render_helper::render_items(
            &mut self.output_device,
            state,
            &self.style,
            &render_context,
        )?;

        render_helper::move_cursor_back_to_start(
            &mut self.output_device,
            render_context.items_viewport_height,
            render_context.header_viewport_height,
        )?;

        lock_output_device_as_mut!(self.output_device)
            .flush()
            .into_diagnostic()?;

        Ok(())
    }
}

mod render_helper {
    use super::{AnsiStyledText, ChUnit, Clear, ClearType, CommonResult,
                DEVELOPMENT_MODE, FunctionComponent, GCStringOwned, Header, HowToChoose,
                IS_FOCUSED, IS_NOT_FOCUSED, InlineString, InlineVec,
                MULTI_SELECT_IS_NOT_SELECTED, MULTI_SELECT_IS_SELECTED, MoveToColumn,
                MoveToNextLine, MoveToPreviousLine, OutputDevice, Print, ResetColor,
                SINGLE_SELECT_IS_NOT_SELECTED, SINGLE_SELECT_IS_SELECTED,
                SelectComponent, SetBackgroundColor, SetForegroundColor, State,
                StyleSheet, TuiStyle, ast, ch, choose_apply_style,
                clip_string_to_width_with_ellipsis, col, fg_blue, get_spaces,
                get_terminal_width, inline_string, queue_commands, usize, width};

    pub struct RenderContext {
        pub header_viewport_height: ChUnit,
        pub items_viewport_height: ChUnit,
        pub viewport_width: ChUnit,
        pub start_display_col_offset: usize,
        pub data_row_index_start: ChUnit,
    }

    impl RenderContext {
        pub fn new(component: &SelectComponent, state: &mut State) -> Self {
            let header_viewport_height =
                component.calculate_header_viewport_height(state);
            let items_viewport_height = component.calculate_items_viewport_height(state);
            let viewport_width = calculate_viewport_width(state);
            let start_display_col_offset = 1;
            let data_row_index_start = state.scroll_offset_row_index;

            Self {
                header_viewport_height,
                items_viewport_height,
                viewport_width,
                start_display_col_offset,
                data_row_index_start,
            }
        }
    }

    pub fn calculate_viewport_width(state: &State) -> ChUnit {
        // Try to get the terminal width from state first (since it should be set
        // when resize events occur). If that is not set, then get the terminal
        // width directly.
        let terminal_width = *match state.window_size {
            Some(size) => size.col_width,
            None => get_terminal_width(),
        };

        // Do not exceed the max display width (if it is set).
        if state.max_display_width == ch(0)
            || state.max_display_width > ch(terminal_width)
        {
            ch(terminal_width)
        } else {
            state.max_display_width
        }
    }

    pub fn log_render_debug_info(state: &State, render_context: &RenderContext) {
        DEVELOPMENT_MODE.then(|| {
            // % is Display, ? is Debug.
            tracing::info! {
                message = "🍎🍎🍎\n render()::state",
                details = %inline_string!(
                    "\t[raw_caret_row_index: {a}, scroll_offset_row_index: {b}], \n\theader_viewport_height: {c}, items_viewport_height:{d}, viewport_width:{e}",
                    a = fg_blue(&inline_string!("{:?}", state.raw_caret_row_index)),
                    b = fg_blue(&inline_string!("{:?}", state.scroll_offset_row_index)),
                    c = fg_blue(&inline_string!("{:?}", render_context.header_viewport_height)),
                    d = fg_blue(&inline_string!("{:?}", render_context.items_viewport_height)),
                    e = fg_blue(&inline_string!("{:?}", render_context.viewport_width)),
                )
            };
        });
    }

    pub fn render_header(
        output_device: &mut OutputDevice,
        header: &Header,
        header_style: &TuiStyle,
        viewport_width: ChUnit,
        start_display_col_offset: usize,
    ) -> CommonResult<()> {
        match header {
            Header::SingleLine(header_text) => render_single_line_header(
                output_device,
                header_text,
                header_style,
                viewport_width,
                start_display_col_offset,
            ),
            Header::MultiLine(header_lines) => {
                render_multi_line_header(output_device, header_lines, viewport_width)
            }
        }
    }

    fn render_single_line_header(
        output_device: &mut OutputDevice,
        header_text: &str,
        header_style: &TuiStyle,
        viewport_width: ChUnit,
        start_display_col_offset: usize,
    ) -> CommonResult<()> {
        let mut header_text =
            format!("{}{}", &get_spaces(start_display_col_offset), header_text);

        header_text = clip_string_to_width_with_ellipsis(header_text, viewport_width);

        queue_commands! {
            output_device,
            // Bring the caret back to the start of line.
            MoveToColumn(0),
            // Reset the colors that may have been set by the previous command.
            ResetColor,
        };

        if let Some(fg) = header_style.color_fg {
            queue_commands! {
                output_device,
                // Set the fg color for the text.
                choose_apply_style!(fg => fg),
            };
        }

        if let Some(bg) = header_style.color_bg {
            queue_commands! {
                output_device,
                // Set the bg color for the text.
                choose_apply_style!(bg => bg),
            };
        }

        queue_commands! {
            output_device,
            // Style the text.
            choose_apply_style!(header_style => bold),
            choose_apply_style!(header_style => italic),
            choose_apply_style!(header_style => dim),
            choose_apply_style!(header_style => underline),
            choose_apply_style!(header_style => reverse),
            choose_apply_style!(header_style => hidden),
            choose_apply_style!(header_style => strikethrough),
            // Clear the current line.
            Clear(ClearType::CurrentLine),
            // Print the text.
            Print(header_text),
            // Move to next line.
            MoveToNextLine(1),
            // Reset the colors.
            ResetColor,
        };

        Ok(())
    }

    fn render_multi_line_header(
        output_device: &mut OutputDevice,
        header_lines: &InlineVec<InlineVec<AnsiStyledText>>,
        viewport_width: ChUnit,
    ) -> CommonResult<()> {
        // Subtract 3 from viewport width because we need to add "..." to the
        // end of the line.
        let mut available_space_col_count: ChUnit = viewport_width - 3;

        // This is the vector of vectors of AnsiStyledText we want to print to
        // the screen.
        let mut multi_line_header_clipped_vec =
            InlineVec::<InlineVec<AnsiStyledText>>::with_capacity(header_lines.len());

        let mut maybe_clipped_text_vec: InlineVec<InlineVec<InlineString>> =
            InlineVec::with_capacity(header_lines.len());

        for header_line in header_lines {
            let mut header_line_modified = InlineVec::new();

            'inner: for span_in_header_line in header_line {
                let span_text = &span_in_header_line.text;
                let span_text_gcs = GCStringOwned::from(span_text);
                let span_us_display_width = *span_text_gcs.display_width;

                // If this span exceeds available space, clip it and stop
                // processing the rest of the spans in this line.
                if span_us_display_width > available_space_col_count {
                    // Clip the text to available space.
                    let clipped_text_str =
                        span_text_gcs.clip(col(0), width(available_space_col_count));
                    let clipped_text = inline_string!("{clipped_text_str}...");
                    header_line_modified.push(clipped_text);
                    break 'inner;
                }

                available_space_col_count -= span_us_display_width;

                // If last item in the header, then fill the remaining
                // space with spaces.
                let maybe_header_line_last_span: Option<&AnsiStyledText> =
                    header_line.last();

                if let Some(header_line_last_span) = maybe_header_line_last_span {
                    if span_in_header_line == header_line_last_span {
                        // Because text is not clipped, we add back the 3
                        // we subtracted earlier for the "...".
                        let num_of_spaces: ChUnit = available_space_col_count + ch(3);

                        let mut span_with_spaces = span_text.to_owned();
                        span_with_spaces.push_str(&get_spaces(num_of_spaces.as_usize()));

                        header_line_modified.push(span_with_spaces);
                    } else {
                        header_line_modified.push(span_text.to_owned());
                    }
                }
            }

            // Reset the available space.
            available_space_col_count = viewport_width - 3;
            maybe_clipped_text_vec.push(header_line_modified);
        }

        // Replace the text inside vector of vectors of AnsiStyledText with
        // the clipped text.
        let zipped = maybe_clipped_text_vec.iter().zip(header_lines.iter());
        zipped.for_each(|(clipped_text_vec, header_span_vec)| {
            let mut ansi_styled_text_vec: InlineVec<AnsiStyledText> = InlineVec::new();
            let zipped = clipped_text_vec.iter().zip(header_span_vec.iter());
            zipped.for_each(|(clipped_text, header_span)| {
                ansi_styled_text_vec.push(ast(clipped_text, header_span.styles.clone()));
            });
            multi_line_header_clipped_vec.push(ansi_styled_text_vec);
        });

        let multi_line_header_text = multi_line_header_clipped_vec
            .iter()
            .map(|header_line| {
                header_line
                    .iter()
                    .map(ToString::to_string)
                    .collect::<String>()
            })
            .collect::<Vec<String>>()
            .join("\r\n");

        queue_commands! {
            output_device,
            // Bring the caret back to the start of line.
            MoveToColumn(0),
            // Reset the colors that may have been set by the previous command.
            ResetColor,
            // Clear the current line.
            Clear(ClearType::CurrentLine),
            // Print each AnsiStyledText.
            Print(multi_line_header_text),
            // Move to next line.
            MoveToNextLine(1),
            // Reset the colors.
            ResetColor,
        };

        Ok(())
    }

    pub fn render_items(
        output_device: &mut OutputDevice,
        state: &State,
        style: &StyleSheet,
        render_context: &RenderContext,
    ) -> CommonResult<()> {
        // Print each line in viewport.
        for viewport_row_index in 0..*render_context.items_viewport_height {
            let row_context = ItemRowContext::new(
                ch(viewport_row_index),
                render_context.data_row_index_start,
                state,
            );

            let selection_state = determine_selection_state(&row_context, state);
            let data_style = get_style_for_selection_state(selection_state, style);

            let row_prefix = create_row_prefix(
                &row_context,
                state.selection_mode,
                render_context.start_display_col_offset,
            );

            render_single_item(
                output_device,
                &row_context,
                &row_prefix,
                &data_style,
                render_context.viewport_width,
            )?;
        }

        Ok(())
    }

    #[derive(Debug, Clone, Copy)]
    enum SelectionStateStyle {
        FocusedAndSelected,
        Focused,
        Selected,
        Unselected,
    }

    #[derive(Debug, Clone, Copy)]
    enum Select {
        Yes,
        No,
    }

    #[derive(Debug, Clone, Copy)]
    enum Focus {
        Yes,
        No,
    }

    struct ItemRowContext {
        data_item: String,
        selected: Select,
        focused: Focus,
    }

    impl ItemRowContext {
        fn new(
            viewport_row_index: ChUnit,
            data_row_index_start: ChUnit,
            state: &State,
        ) -> Self {
            let data_row_index: usize =
                (data_row_index_start + viewport_row_index).into();
            let caret_row_scroll_adj = viewport_row_index + state.scroll_offset_row_index;
            let data_item = state.items[data_row_index].to_string();

            let selected = if state.selected_items.iter().any(|item| item == &data_item) {
                Select::Yes
            } else {
                Select::No
            };

            let focused = if caret_row_scroll_adj == state.get_focused_index() {
                Focus::Yes
            } else {
                Focus::No
            };

            Self {
                data_item,
                selected,
                focused,
            }
        }
    }

    fn determine_selection_state(
        row_context: &ItemRowContext,
        _state: &State,
    ) -> SelectionStateStyle {
        match (row_context.focused, row_context.selected) {
            (Focus::Yes, Select::Yes) => SelectionStateStyle::FocusedAndSelected,
            (Focus::Yes, Select::No) => SelectionStateStyle::Focused,
            (Focus::No, Select::Yes) => SelectionStateStyle::Selected,
            (Focus::No, Select::No) => SelectionStateStyle::Unselected,
        }
    }

    fn get_style_for_selection_state(
        selection_state: SelectionStateStyle,
        style: &StyleSheet,
    ) -> TuiStyle {
        match selection_state {
            SelectionStateStyle::FocusedAndSelected => style.focused_and_selected_style,
            SelectionStateStyle::Focused => style.focused_style,
            SelectionStateStyle::Selected => style.selected_style,
            SelectionStateStyle::Unselected => style.unselected_style,
        }
    }

    fn create_row_prefix(
        row_context: &ItemRowContext,
        selection_mode: HowToChoose,
        start_display_col_offset: usize,
    ) -> String {
        let padding_left = get_spaces(start_display_col_offset);

        match selection_mode {
            HowToChoose::Single => {
                if let Focus::Yes = row_context.focused {
                    format!("{} {SINGLE_SELECT_IS_SELECTED} ", &padding_left)
                } else {
                    format!("{} {SINGLE_SELECT_IS_NOT_SELECTED} ", &padding_left)
                }
            }
            HowToChoose::Multiple => match (row_context.focused, row_context.selected) {
                (Focus::Yes, Select::Yes) => {
                    format!("{} {IS_FOCUSED} {MULTI_SELECT_IS_SELECTED} ", &padding_left)
                }
                (Focus::Yes, Select::No) => {
                    format!(
                        "{} {IS_FOCUSED} {MULTI_SELECT_IS_NOT_SELECTED} ",
                        &padding_left
                    )
                }
                (Focus::No, Select::Yes) => {
                    format!(
                        "{} {IS_NOT_FOCUSED} {MULTI_SELECT_IS_SELECTED} ",
                        &padding_left
                    )
                }
                (Focus::No, Select::No) => {
                    format!(
                        "{} {IS_NOT_FOCUSED} {MULTI_SELECT_IS_NOT_SELECTED} ",
                        &padding_left
                    )
                }
            },
        }
    }

    fn render_single_item(
        output_device: &mut OutputDevice,
        row_context: &ItemRowContext,
        row_prefix: &str,
        data_style: &TuiStyle,
        viewport_width: ChUnit,
    ) -> CommonResult<()> {
        let data_item = format!("{row_prefix}{}", row_context.data_item);
        let data_item: String =
            clip_string_to_width_with_ellipsis(data_item, viewport_width);
        let data_item_gcs = GCStringOwned::from(&data_item);
        let data_item_display_width: ChUnit = *data_item_gcs.display_width;
        let padding_right = if data_item_display_width < viewport_width {
            get_spaces(usize(viewport_width - data_item_display_width))
        } else {
            get_spaces(0)
        };

        queue_commands! {
            output_device,
            // Bring the caret back to the start of line.
            MoveToColumn(0),
            // Reset the colors that may have been set by the previous command.
            ResetColor,
            // Clear the current line.
            Clear(ClearType::CurrentLine),
        };

        if let Some(fg) = data_style.color_fg {
            queue_commands! {
                output_device,
                // Set the fg color for the text.
                choose_apply_style!(fg => fg),
            };
        }

        if let Some(bg) = data_style.color_bg {
            queue_commands! {
                output_device,
                // Set the bg color for the text.
                choose_apply_style!(bg => bg),
            };
        }

        queue_commands! {
            output_device,
            // Style the text.
            choose_apply_style!(data_style => bold),
            choose_apply_style!(data_style => italic),
            choose_apply_style!(data_style => dim),
            choose_apply_style!(data_style => underline),
            choose_apply_style!(data_style => reverse),
            choose_apply_style!(data_style => hidden),
            choose_apply_style!(data_style => strikethrough),
            // Print the text.
            Print(data_item),
            // Print the padding text.
            Print(padding_right),
            // Move to next line.
            MoveToNextLine(1),
            // Reset the colors.
            ResetColor,
        };

        Ok(())
    }

    pub fn move_cursor_back_to_start(
        output_device: &mut OutputDevice,
        items_viewport_height: ChUnit,
        header_viewport_height: ChUnit,
    ) -> CommonResult<()> {
        queue_commands! {
            output_device,
            MoveToPreviousLine(*items_viewport_height + *header_viewport_height),
        };
        Ok(())
    }
}

fn clip_string_to_width_with_ellipsis(
    header_text: String,
    viewport_width: ChUnit,
) -> String {
    let header_text_gcs = GCStringOwned::from(&header_text);
    let header_text_display_width = header_text_gcs.display_width;
    let available_space_col_count: ChUnit = viewport_width;
    if *header_text_display_width > available_space_col_count {
        // Clip the text to available space.
        let clipped_text =
            header_text_gcs.clip(col(0), width(available_space_col_count - 3));
        let clipped_text = format!("{clipped_text}...");
        return clipped_text;
    }
    header_text
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;
    use serial_test::serial;

    use super::*;
    use crate::{ColorSupport, ItemsOwned, OutputDeviceExt,
                global_color_support::{clear_override, set_override}};

    #[test]
    fn test_clip_string_to_width_with_ellipsis() {
        let line = "This is a long line that needs to be clipped".to_string();
        let clipped_line =
            clip_string_to_width_with_ellipsis(line.clone(), ChUnit::new(20));
        assert_eq!(clipped_line, "This is a long li...");

        let short_line = "This is a short line".to_string();
        let clipped_short_line =
            clip_string_to_width_with_ellipsis(short_line.clone(), ChUnit::new(20));
        assert_eq!(clipped_short_line, "This is a short line");
    }

    #[serial]
    #[test]
    fn test_select_component() {
        let mut state = State {
            header: Header::SingleLine("Header".into()),
            items: ItemsOwned::from(&["Item 1", "Item 2", "Item 3"]),
            max_display_height: ch(5),
            max_display_width: ch(40),
            raw_caret_row_index: ch(0),
            scroll_offset_row_index: ch(0),
            selected_items: ItemsOwned::new(),
            selection_mode: HowToChoose::Single,
            ..Default::default()
        };

        state.scroll_offset_row_index = ch(0);

        let (output_device, stdout_mock) = OutputDevice::new_mock();

        let mut component = SelectComponent {
            output_device,
            style: StyleSheet::default(),
        };

        set_override(ColorSupport::Ansi256);
        component.render(&mut state).unwrap();

        let generated_output = stdout_mock.get_copy_of_buffer_as_string();

        println!("generated_output = writer.get_buffer(): \n\n{generated_output:#?}\n\n");

        let expected_output = "\u{1b}[4F\u{1b}[1G\u{1b}[0m\u{1b}[38;5;153m\u{1b}[48;5;235m\u{1b}[21m\u{1b}[23m\u{1b}[22m\u{1b}[24m\u{1b}[27m\u{1b}[28m\u{1b}[29m\u{1b}[2K Header\u{1b}[1E\u{1b}[0m\u{1b}[1G\u{1b}[0m\u{1b}[2K\u{1b}[38;5;46m\u{1b}[21m\u{1b}[23m\u{1b}[22m\u{1b}[24m\u{1b}[27m\u{1b}[28m\u{1b}[29m  ◉ Item 1                              \u{1b}[1E\u{1b}[0m\u{1b}[1G\u{1b}[0m\u{1b}[2K\u{1b}[21m\u{1b}[23m\u{1b}[22m\u{1b}[24m\u{1b}[27m\u{1b}[28m\u{1b}[29m  ◌ Item 2                              \u{1b}[1E\u{1b}[0m\u{1b}[1G\u{1b}[0m\u{1b}[2K\u{1b}[21m\u{1b}[23m\u{1b}[22m\u{1b}[24m\u{1b}[27m\u{1b}[28m\u{1b}[29m  ◌ Item 3                              \u{1b}[1E\u{1b}[0m\u{1b}[4F";
        assert_eq!(generated_output, expected_output);

        clear_override();
    }
}
